# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""changebranch - stuff for change branches

Classes:
ChangeBranch -- representation of a change branch

Functions:
ListChangeBranches -- generate changebranch names and dirents
Diff            -- diff a change branch

"""

import os
import posixpath
import random
import re
import shutil
import subprocess
import sys
import tempfile

from errno import EEXIST

import svn.ra
import svn.wc

from svn.core import SVN_DIRENT_CREATED_REV, SVN_DIRENT_KIND
from svn.core import SWIG_SVN_INVALID_REVNUM as SVN_INVALID_REVNUM
from svn.core import svn_node_dir, svn_node_none
from svn.core import svn_prop_regular_kind, svn_property_kind

import gvn.commit
import gvn.errors
import gvn.platform
import gvn.repository
import gvn.svncmd
import gvn.util
import gvn.wc


_DIFF_COMMAND = 'diff'


class _WCBranch(gvn.wc.Edit):
  """Apply a change from a working copy to a change branch."""

  def __init__(self, *args, **kwargs):
    """Same as gvn.wc.Edit.__init__ plus source_revision.

    Additional arguments:
    source_revision -- revision of source branch copied to change branch
                       (required keyword argument)
    """

    self.source_revision = kwargs.pop('source_revision')
    gvn.wc.Edit.__init__(self, *args, **kwargs)

  def _GetAction(self):
    """gvn.wc.Edit._GetAction but treat conflicted as modified."""
    try:
      action = gvn.wc.Edit._GetAction(self)
    except KeyError, e:
      if e.args[0] != svn.wc.status_conflicted:
        raise
      action = self._Modified
    return action

  def _PostProcess(self, pool):
    state = gvn.wc.ChangeState(None, self.wc.AbsolutePath(self.wc_path),
                               None, self.status)
    self.wc.UpdateChangeState(self.wc_path, state)

  def _Modified(self, parent, path, editor, pool):
    if (self.source_revision is not None
        and self.status.entry.revision != self.source_revision):
      self.wc.Notify(self.wc_path, svn.wc.notify_commit_modified,
                     self.status.entry.kind, pool)
      # If wc_path's base revision is not the same as the
      # source_revision the overall change branch is based on, replace
      # this node in the change branch with a copy from wc_path's base
      # and base change on that.
      if not self.status.entry.repos.startswith('http'):
        # As long as we need the 2939 hack in _AddHelper, only for non-http.
        editor.delete_entry(path, self.status.entry.revision, parent, pool)
      return self._AddHelper(parent, path, editor,
                             self.status.entry.url,      # copyfrom
                             self.status.entry.revision, # copyfrom
                             pool)
    else:
      return gvn.wc.Edit._Modified(self, parent, path, editor, pool)

  # XXX workaround http://subversion.tigris.org/issues/show_bug.cgi?id=2939
  def _AddHelper(self, parent, path, editor,
                 copyfrom_url=None, copyfrom_rev=None, pool=None):
    self.DiagLog('')
    if self.status.entry.repos.startswith('http'):
      args = (path, self.status.entry.revision, parent, pool)
      gvn.DiagLog('delete_entry%s; ', args)
      editor.delete_entry(*args)
    result = gvn.wc.Edit._AddHelper(self, parent, path, editor,
                                    copyfrom_url, copyfrom_rev, pool)
    gvn.DiagLog('\n')
    return result


class PostfixTextDeltaManager(gvn.wc.PostfixTextDeltaManager):
  def PostProcess(self, wc_path, status, checksum):
    """Update gvn change state.

    No wc bump, as this isn't a commit to the working copy's URL.
    """

    state = gvn.wc.ChangeState(None, self.wc.AbsolutePath(wc_path),
                               checksum, status)
    self.wc.UpdateChangeState(wc_path, state)

  def HandleTmp(self, tmp):
    """Unlink tmp file, rather than ignoring it.

    For snapshots, we must remove this file, otherwise later submits try to
    use it as the new text base, resulting in corruption.
    """
    os.unlink(tmp)


class ChangeBranch(object):
  """Representation of a change branch."""

  def __init__(self, config, project, name, username=None, revision=None):
    """Initialize ChangeBranch, which may or may not exist in the repo.

    Arguments:
    config   -- gvn.config.Config
    project  -- gvn.project.Project
    name     -- name of change branch
    username -- username; defaults to config.username
    revision -- revision number of existing change branch

    """

    gvn.util.ValidateChangeName(name)
    self._name = name
    self._config = config
    self._project = project

    if username is None:
      self._username = self._project.repository.username
    else:
      self._username = username

    if revision is None:
      self._revision = revision
    else:
      # TODO(epg): We should fill this in on the fly.
      (self._revision,) = self._project.repository.GetRevisions(start=revision)

    self._description = None
    self._revisions = []

  def GetInstanceAtRevision(self, revision=None):
    """Return an instance of this changebranch at revision 'revision'.

    The current revision of this changebranch instance is assumed if no
    revision is specified.

    """

    if revision is None and self._revision is not None:
        revision = self._revision.number
    return ChangeBranch(self._config, self._project, self._name,
                        self._username, revision)

  name = property(lambda self: self._name)
  username = property(lambda self: self._username)
  change_name_at_head = property(lambda self: '/'.join([self.username,
                                                        self.name]))
  change_name = property(lambda self: '@'.join(['/'.join([self.username,
                                                          self.name]),
                                                str(self.revision.number)]))

  def _GetRevision(self):
    if self._revision is None:
      dirent = self._project.repository.Stat(
                            self._project.ChangeBranchPath(self.username,
                                                           self.name),
                            self._project.repository.GetHead())
      self._revision = dirent.last_changed
    return self._revision
  revision = property(_GetRevision)

  def _GetRevisions(self):
    """Return the equivalent of:

           svn log --stop-on-copy ^/changes/<username>/<branchname>@revision

    This will be a list of repository.revision objects, sorted in order
    of decreasing repository.revision.number (i.e. from youngest to oldest).
    """
    if not self._revisions:
      self._revisions = self._project.repository.GetRevisions(
          paths=[self._project.ChangeBranchPath(self.username, self.name)],
          peg=self.revision.number,
          strict_node_history=True)
    return self._revisions
  revisions = property(_GetRevisions)

  def _GetDescription(self):
    if self._description is None:
      try:
        revision = self.revision
      except gvn.errors.RepoPath:
        return
      self._description = revision['svn:log']
    return self._description
  description = property(_GetDescription)

  def _GetChangedPaths(self):
    try:
      revision = self.revision
    except gvn.errors.RepoPath:
      return []

    cbp = self._project.ChangeBranchPath(self.username, self.name)

    # TODO(epg): Er, isn't this entirely redundant?  Just use self.revision...
    (revision,) = self._project.repository.GetRevisions([cbp],
                                                        start=revision.number,
                                                        end=1, limit=1)

    changes = []
    topname = None
    for i in sorted(revision.paths, key=lambda x: x.path):
      # When we start through here, we don't know the "top name"
      # ('trunk' for '^/trunk', 'projects' for '^/public/projects').
      if topname is None:
        (dirname, basename) = posixpath.split(i.path.lstrip('/'))
        if dirname == cbp:
          # Found the branch; save topname, path, and copyfrom.
          topname = basename
          cbp = self._project.ChangeBranchPath(self.username, self.name,
                                               topname)
          source = i.copyfrom_path
          source_revision = i.copyfrom_revision
          continue
        # If we get here, we're looking at creation of directories
        # above the changebranch; skip those.
        continue
      try:
        relative_path = gvn.util.RelativePath(cbp, i.path.lstrip('/'))
      except gvn.errors.PathNotChild:
        # i is not in our changebranch
        continue
      changes.append(gvn.repository.ChangedPath(i.path, i.revision, i.action,
                                            i.copyfrom_path, i.copyfrom_revision,
                                            relative_path, source,
                                            source_revision))

    return changes
  changed_paths = property(_GetChangedPaths, doc="""List of ChangedPaths.""")

  def ChangedPathsRelative(self):
    """Return iterator returning relative paths of changed_paths."""
    return (x.relative_path for x in self.changed_paths)

  def Exists(self):
    try:
      # Contact the repository to find the last changed revision.
      self.revision
      return True
    except gvn.errors.RepoPath:
      # Changebranch does not exist.
      return False

  def _MissingDirectories(self, path, revision, pool):
    """Return list of directories to make in order to mkdir path.

    In an empty repository:
      _MissingDirectories(foo/bar/baz)  => [foo, foo/bar, foo/bar/baz]
    If foo/bar already exists:
      _MissingDirectories(foo)          => []
      _MissingDirectories(foo/bar)      => []
      _MissingDirectories(foo/bar/baz)  => [foo/bar/baz]

    Arguments:
    path                -- path relative to repository root
    revision            -- revision the transaction is based on
    pool                -- memory pool
    """
    if path == '':
      return []
    kind = svn.ra.check_path(self._project.repository.ra, path, revision, pool)
    if kind != svn_node_none:
      return []
    return (self._MissingDirectories(posixpath.dirname(path), revision, pool)
            + [path])

  def Branch(self, _wc, paths=None, description=None, pool=None):
    """Replace (or create) change branch with paths.

    paths may be either a dict mapping path to svn_wc_status2_t or a
    list of paths.  If it is a list, .Branch will open the deepest
    common path and discover the svn_wc_status2_t for each path
    itself; if a dict, .Branch assumes the wc is already open.

    Arguments:
    paths       -- list of paths to change; defaults to .changed_paths
    description -- svn:log for change; defaults to .description; default ''
    pool        -- memory pool

    """

    if _wc.name == '':
      raise gvn.errors.BranchFromRoot

    # Save the current changebranched paths so we can remove local
    # meta-data for paths being removed from the changebranch.
    orig_paths = set(self.ChangedPathsRelative())

    if paths is None:
      paths = orig_paths

    if not hasattr(paths, 'iterkeys'):
      # Treat it as a list.
      targets = paths
      _wc.Open(targets)
      paths = {}
      def status_cb(target, status):
        paths[_wc.RelativePath(target)] = status
      _wc.Status(targets, recursive=False, get_all=False, no_ignore=False,
                 show_children=False, callback=status_cb)

    if description is None:
      description = self.description
      if description is None:
        description = ''

    head = self._project.repository.GetHead(pool)

    changes_path = self._project.ChangeBranchPath()
    user_path = self._project.ChangeBranchPath(self.username)
    container_path = self._project.ChangeBranchPath(self.username, self.name)
    branch_path = self._project.ChangeBranchPath(self.username, self.name,
                                                 _wc.name)

    mkdirs = self._MissingDirectories(container_path, head, pool)
    delete_branch_path = (container_path not in mkdirs
                          and svn.ra.check_path(self._project.repository.ra,
                                                branch_path, head,
                                                pool) != svn_node_none)
    repo_paths = list(mkdirs)
    repo_paths.append(branch_path)
    for i in sorted(paths.iterkeys()):
      repo_paths.append(self._project.ChangeBranchPath(self.username,
                                                       self.name,
                                                       '/'.join([_wc.name,
                                                                 i])))

    source_revision = _wc.Entry(pool=pool).revision

    deltaman = PostfixTextDeltaManager(_wc)
    def action(path, pool):
      if path in mkdirs:
        return gvn.commit.Mkdir(head)
      elif path == branch_path:
        return gvn.commit.CopyDir(_wc.URL(), source_revision,
                                  replace=delete_branch_path,
                                  replace_revision=head)
      wc_path = path[len(branch_path)+1:]
      try:
        if _wc.change_state[wc_path].change_name != self.name:
          # ye olde stupid generic exception; we gotta fix that
          raise gvn.errors.ChangeBranch('%s already in %s' % (wc_path,
                                        _wc.change_state[wc_path].change_name))
      except KeyError:
        # not yet changebranched at all
        pass
      return _WCBranch(_wc, wc_path, paths[wc_path],
                       source_revision=source_revision,
                       file_baton_cb=deltaman.Add)
    try:
      try:
        commit_info = gvn.commit.Drive(self._project.repository,
                                       {'svn:log': description},
                                       head, repo_paths, action,
                                       deltaman.Transmit)
      except svn.core.SubversionException, e:
        # Try to report decent error message for out of date parent.
        if e.apr_err == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
          # local and svn use this
          m = re.match(r"Path '(.*)' not present$", e.args[0])
          if m is None:
            # Not a case we know about; let it out.
            raise
          parent = m.group(1)[len(branch_path)+1:]
          raise gvn.errors.OutOfDateParent(e, _wc.subpath, source_revision,
                                           parent)
        elif e.apr_err == svn.core.SVN_ERR_FS_NOT_FOUND:
          # neon (TODO(epg): and serf?) use this
          m = re.match(r"File not found: transaction '[^']+', path '(.*)'$",
                       e.args[0])
          if m is None:
            # Not a case we know about; let it out.
            raise
          # TODO(epg): neon has leading /; does serf?
          parent = posixpath.dirname(m.group(1)[len(branch_path)+2:])
          raise gvn.errors.OutOfDateParent(e, _wc.subpath, source_revision,
                                           parent)
        # Not a case we know about; let it out.
        raise
    except:
      _wc.ClearPendingChangeState()
      raise

    # Update local change state.
    for path in orig_paths.difference(paths):
      # User removed path from changebranch.
      _wc.DeleteChangeState(path)
    for i in paths:
      _wc.UpdateChangeState(i, change_name=self.name)
    _wc.Save()
    _wc.Close()

    self._description = description
    # TODO(epg): We just created this revision, so we can construct
    # the Revision object without having to svn log.
    (self._revision,) = self._project.repository.GetRevisions(start=commit_info.revision)

    if callable(_wc.notify_func):
      _wc.notify_func('Changed %s.\n' % (self.change_name,))
      if commit_info.post_commit_err is not None:
        _wc.notify_func('\nWarning: %s\n' % (commit_info.post_commit_err,))

  def Delete(self, _wc, description=None, pool=None):
    """Delete, leaving self invalid and paths associated with no change.

    Arguments:
    description -- svn:log for deletion; defaults to .description or ''
    pool        -- memory pool

    """

    # Save this for updating _wc.change_state .
    paths = list(self.ChangedPathsRelative())

    if description is None:
      description = 'Delete.\n'

    head = self._project.repository.GetHead(pool)
    container_path = self._project.ChangeBranchPath(self.username, self.name)

    def action(path, pool):
      return gvn.commit.Delete(self.revision.number)
    try:
      commit_info = gvn.commit.Drive(self._project.repository,
                                     {'svn:log': description},
                                     head, [container_path], action, pool=pool)
    except:
      _wc.ClearPendingChangeState()
      raise

    # Update local change state.
    for i in paths:
      _wc.DeleteChangeState(i)
    _wc.Save()

    self._revision = self._description = None

    if callable(_wc.notify_func):
      _wc.notify_func('Deleted %s@%d.\n' % (self.change_name_at_head,
                                            commit_info.revision))
      if commit_info.post_commit_err is not None:
        _wc.notify_func('\nWarning: %s\n' % (commit_info.post_commit_err,))

  def Submit(self, _wc, description=None, pool=None):
    """Submit (and remove) change branch.

    Arguments:
    description -- svn:log for submit; defaults to .description or ''
    pool        -- memory pool

    """

    if description is None:
      description = self.description
      if description is None:
        description = ''

    head = self._project.repository.GetHead(pool)

    if callable(_wc.notify_func):
      _wc.notify_func('Submitting %s\n' % (self.change_name,), pool)

    container_path = self._project.ChangeBranchPath(self.username, self.name)

    paths = []
    repo_paths = [container_path]
    for i in self.changed_paths:
      paths.append(i.relative_path)
      repo_paths.append(_wc.RepoPath(i.relative_path))

    _wc.Open(paths, write_lock=True)

    path_status = {}
    def status_cb(target, status):
      wc_path = _wc.RelativePath(target)
      # TODO(epg): Need to check for other kinds of status as well.
      # For example, if i have a changebranch in which i add new/ and
      # new/file and someone else commits new/ i can't submit (out of
      # date).  If i update and snapshot i'll be fine, but if i update
      # and forget to snapshot, status here is svn_wc_status_none for
      # new/ (because it's an Add on cb but unmodified in wc) which
      # causes _GetAction to have a KeyError.
      if gvn.wc.IsConflicted(status):
        raise gvn.errors.Conflict(wc_path)
      path_status[wc_path] = status
    _wc.Status(paths, recursive=False, get_all=False, no_ignore=False,
               show_children=False, callback=status_cb)

    deltaman = gvn.wc.PostfixTextDeltaManager(_wc)
    def action(path, editpool):
      if path == container_path:
        return gvn.commit.Delete(self.revision.number)
      wc_path = _wc.LocalPath(path)
      return gvn.wc.Edit(_wc, wc_path, path_status[wc_path],
                         file_baton_cb=deltaman.Add)
    try:
      commit_info = gvn.commit.Drive(self._project.repository,
                                     {'svn:log': description,
                                      'gvn:change': self.change_name},
                                     head, repo_paths, action,
                                     deltaman.Transmit, pool)
    except:
      _wc.ClearPendingChangeState()
      raise

    # TODO(epg): Any of these may fail independently: MarkSubmitted,
    # .gvnstate updating, wc bump, and anything else we add here.  But
    # we want to try them all, anyway, but let the callers see all
    # exceptions.  Collin agreed that a composite exception class,
    # containing a list of exception objects for caller to handle, is
    # a sane way to cover this.  For now, ignore MarkSubmitted errors.
    # Eventually, the post-commit processor will add missing
    # gvn:submitted properties.
    try:
      self.MarkSubmitted(commit_info.revision, pool)
    except:
      #import traceback; traceback.print_exc()
      pass

    # Update local change state.
    for i in paths:
      _wc.DeleteChangeState(i)
    _wc.Save()

    if callable(_wc.notify_func):
      _wc.notify_func('Submitted r%d.\n' % (commit_info.revision,), pool)
      if commit_info.post_commit_err is not None:
        _wc.notify_func('\nWarning: %s\n' % (commit_info.post_commit_err,))

    # TODO(epg): Huh?  Why are we clobbering these?
    self._revision = self._description = None

    _wc.ProcessCommittedQueue(commit_info, pool)
    _wc.Close()

    return commit_info.revision

  def Acknowledge(self, pool):
    """Mark this change branch acknowledged (reviewed, approved, ...)."""

    propname = ':'.join(['gvn:approve', self._project.repository.username])
    svn.ra.change_rev_prop(self._project.repository.ra,
                           self.revision.number, propname, '',
                           pool)

  def MarkSubmitted(self, revision, pool):
    """Mark this changebranch submitted in revision."""
    svn.ra.change_rev_prop(self._project.repository.ra,
                           self.revision.number, 'gvn:submitted',
                           str(revision), pool)

def ListChangeBranches(project, dirent_fields=0, user=None, pool=None):
  """Return a generator yielding (change_name, svn_dirent_t) tuples.

  Arguments:
  project               -- gvn.project.Project
  dirent_fields         -- see svn_ra_get_dir2; use SVN_DIRENT_CREATED_REV
                           and SVN_DIRENT_KIND even if not specified here
  user                  -- user whose changebranches to yield;
                           yield all if None (the default)
  pool                  -- memory pool

  Raises:
  gvn.errors.NoChangeBranchBase
  gvn.errors.NoUserChangeBranch
  svn.core.SubversionException
  """
  if user is None:
    try:
      (dirents, revision, properties) = svn.ra.get_dir2(project.repository.ra,
                                                    project.ChangeBranchPath(),
                                                        SVN_INVALID_REVNUM,
                                                        SVN_DIRENT_KIND,
                                                        pool)
    except svn.core.SubversionException, e:
      if e.apr_err in [svn.core.SVN_ERR_FS_NOT_FOUND,
                       svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND]:
        raise gvn.errors.NoChangeBranchBase(project.ChangeBranchPath())
      raise
    users = (name for (name, dirent) in dirents.iteritems()
             if dirent.kind == svn_node_dir)
  else:
    users = [user]

  # No matter what the caller wanted, we need at least these two.
  dirent_fields |= SVN_DIRENT_CREATED_REV
  dirent_fields |= SVN_DIRENT_KIND

  iterpool = svn.core.Pool(pool)
  for user in users:
    iterpool.clear()
    try:
      (dirents, revision, properties) = svn.ra.get_dir2(
                                        project.repository.ra,
                                        project.ChangeBranchPath(user),
                                        SVN_INVALID_REVNUM,
                                        dirent_fields, pool)
    except svn.core.SubversionException, e:
      if e.apr_err in [svn.core.SVN_ERR_FS_NOT_FOUND,
                       svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND]:
        raise gvn.errors.NoUserChangeBranch(project.ChangeBranchPath(user))
      raise
    for (name, dirent) in dirents.iteritems():
      if dirent.kind == svn_node_dir:
        yield ('%s/%s@%d' % (user, name, dirent.created_rev), dirent)
  iterpool.destroy()

def UniqueChangeName(project, username, base, pool):
  """Return a unique change name starting with base.

  If base is available, just return that, otherwise try appending
  numbers to it until finding a unique name.

  """

  try:
    changebranches = [change_name for (change_name, dirent) in
                      ListChangeBranches(project, user=username, pool=pool)]
  except gvn.errors.NoUserChangeBranch:
    return base
  if base not in changebranches:
    return base

  i = 1
  while True:
    name = base + str(i)
    if name not in changebranches:
      return name
    i += 1

def RandomBranchName(length=4):
  """Return a random branchname of length 'length'.

  Branchnames are only lowercase characters and numbers, excluding:
     lower case 'o' and the number 0 so as not to be confused
     lower case 'l' and the number 1 so as not to be confused

  Letters and numbers alternate beginning with a letter.

  Examples of a 4 character branchname:  'a2i3', 'w2b8', 'd5j4'.

  Uses of names returned by this method should be wrapped in
  UniqueChangeName() to ensure uniqueness.

  """

  branchname = []
  for i in xrange(0, length):
    if i % 2 == 0:
      branchname.append(random.choice('abcdefghijkmnpqrstuvwxyz'))
    else:
      branchname.append(random.choice('23456789'))

  return ''.join(branchname)

######################################################################

def _makedirs(filename):
  try:
    os.makedirs(filename)
  except OSError, e:
    if e.errno != EEXIST:
      raise

def _GetFile(fn, repo, path, revision, prop_hack=False, pool=None):
  """Get path from repo at revision into local file fn.

  Arguments:
  fn          -- name of local file to write to
  repo        -- Repository to get path from
  path        -- path to get from repo
  revision    -- revision of path to get
  prop_hack   -- whether to append properties and values to the local file
  pool        -- memory pool
  """

  path = path.lstrip('/')
  if repo.Stat(path, revision).kind == svn_node_dir:
    _makedirs(fn)
    return

  fp = open(fn, 'w')
  (revision, properties) = svn.ra.get_file(repo.ra, path, revision, fp, pool)

  if not prop_hack:
    # XXX temporary hack to see props diff, but only do it if we're in
    # diff source cb mode.
    fp.write('\n\nProperties:\n')
    for name in sorted(properties.iterkeys()):
      (kind, prefix_len) =  svn_property_kind(name)
      if kind != svn_prop_regular_kind:
        continue
      fp.write('%s: %s\n' % (name, properties[name]))

  fp.close()

def MakeLocalDiffTree(repo, cb, from_change_paths=None, wc=None, callback=None,
                      tmpdir=None, pool=None):
  """Build two local trees for diffing; pass file pairs to callback.

  To make this crap even messier, from_change_paths indicates that
  we're diffing cb => wc rather than source => cb; from_change_paths
  may be None or a list of paths to diff.

  Arguments:
  repo              -- Repository to get files from
  cb                -- ChangeBranch whose files to get
  from_change_paths -- list of paths to diff from changebranch to local
  wc                -- WorkingCopy; required if from_change_paths is not None
  callback          -- callable(change, old, new) for each pair
  tmpdir            -- directory to build trees in
  pool              -- memory pool
  """

  for change in cb.changed_paths:
    if from_change_paths is not None:
      # diff cb wc mode
      if change.relative_path not in from_change_paths:
        # Caller didn't ask to diff this file.
        continue

    old_tmp_path = posixpath.join(tmpdir, 'old', change.relative_path)
    old_tmpdir = posixpath.dirname(old_tmp_path)
    new_tmp_path = posixpath.join(tmpdir, 'new', change.relative_path)
    new_tmpdir = posixpath.dirname(new_tmp_path)

    _makedirs(old_tmpdir)
    _makedirs(new_tmpdir)

    if from_change_paths is not None:
      if change.action == 'D':
        new_tmp_path = None
      else:
        # symlink or copy the wc path to be the new path
        if sys.platform == 'win32':
          shutil.copy(wc.AbsolutePath(change.relative_path), new_tmp_path)
        else:
          os.symlink(wc.AbsolutePath(change.relative_path), new_tmp_path)
    else:
      if not (change.action == 'A' and change.copyfrom_path is None):
        try:
          _GetFile(old_tmp_path, repo, change.old_path,
                   change.old_revision, from_change_paths is not None, pool)
        except gvn.errors.RepoPath:
          # Some parent of this path is in this change a copy, and now
          # we're deleting this path out of the copy: change.old_path
          # does not exist.  XXX Should we do anything but ignore it?
          continue
      else:
        old_tmp_path = None

    if from_change_paths is not None:
      cb_tmp_path = old_tmp_path
    else:
      cb_tmp_path = new_tmp_path

    if change.action != 'D':
      _GetFile(cb_tmp_path, repo, change.path,
               change.revision, from_change_paths is not None, pool)
    else:
      cb_tmp_path = None

    if from_change_paths is not None:
      old_tmp_path = cb_tmp_path
    else:
      new_tmp_path = cb_tmp_path
    callback(change, old_tmp_path, new_tmp_path)

def Diff(repo, cb, fp, diff_command, from_change_paths=None, wc=None,
         pool=None):
  """Diff source => cb (or cb => wc if from_changes_paths is not None).

  Arguments:
  repo              -- Repository cb is in
  cb                -- ChangeBranch to diff
  fp                -- file-like object to write diff output to (only the
                       write method must exist)
  diff_command      -- 'internal', 'tkdiff', or a custom command to use to
                       diff file pairs
  from_change_paths -- see MakeLocalDiffTree
  wc                -- see MakeLocalDiffTree
  pool              -- memory pool
  """
  if len(cb.changed_paths) == 0:
    return

  if diff_command == 'internal':
    # default, 'internal', diff implementation
    # TODO(epg): Now that we do a separate diff per file, we can
    # easily do property diffs like svn does; none of the svn_diff
    # stuff is yet wrapped (that's why we run diff) so it will be yet
    # more imitation code...
    def callback(change, old, new):
      if change.action == 'D':
        return

      path = change.relative_path
      if old is None:
        old = os.devnull
      if new is None:
        new = os.devnull

      fp.write("""Index: %s
===================================================================
""" % (path,))

      if change.action == 'A' and change.copyfrom_path is None:
        oldpath = change.path
        oldrevision = 0
      else:
        oldpath = change.old_path
        oldrevision = change.old_revision
      # TODO(epg): Use difflib until svn_diff is available?
      try:
        proc = subprocess.Popen([_DIFF_COMMAND, '-u',
                                 '-L', '%s      (/%s@%d)' % (path, oldpath,
                                                             oldrevision),
                                 '-L', '%s      (/%s@%d)' % (path, change.path,
                                                             change.revision),
                                 old, new], stdout=subprocess.PIPE)
      except OSError, e:
        if e.errno not in gvn.platform.ENOENT_codes:
          raise
        raise gvn.errors.NoDiffCommand
      for line in proc.stdout:
        fp.write(line)
  else:
    file_pairs = []
    def callback(change, old, new):
      file_pairs.append((old, new))

  tmpdir = None
  try:
    tmpdir = tempfile.mkdtemp(prefix='gvndiff-')
    MakeLocalDiffTree(repo, cb, from_change_paths, wc, callback, tmpdir, pool)

    if diff_command and os.path.basename(diff_command) == 'tkdiff':
      # Hacky tkdiff implementation.
      argv = []
      first = True
      for (old, new) in file_pairs:
        if not first:
          argv.append(':')
        else:
          first = False

        if old is None:
          old = os.devnull
        argv.append(old)

        if new is None:
          new = os.devnull
        argv.append(new)

      argv.insert(0, diff_command)
      subprocess.call(argv)

    elif diff_command and diff_command != 'internal':
      # We appear to have a custom diff_command.
      for (old, new) in file_pairs:
        argv = []
        if old is None:
          old = os.devnull
        argv.append(old)

        if new is None:
          new = os.devnull
        argv.append(new)

        argv.insert(0, diff_command)
        subprocess.call(argv)
  finally:
    if tmpdir is not None:
      gvn.platform.rmtree(tmpdir)

# TODO(epg): Use bindings.  Extend Notify callbacks to handle
# content_state and prop_state.  Handle conflicts (i'm thinking we
# want to allow conflicted files to be snapshotted without resolving;
# submit already won't let them through...).  It doesn't even check
# svn exit codes.

def _MergeAdd(change, _wc, pool):
  if change.copyfrom_path is None:
    # XXX Why isn't the .lstrip('/') required in _GetFile?  Anywhere else?
    if _wc.project.repository.Stat(change.path.lstrip('/'),
                                   change.revision).KindIsDir():
      gvn.svncmd.RunSvn(['mkdir', '-q', change.relative_path])
    else:
      _GetFile(change.relative_path, _wc.project.repository,
               change.path, change.revision, pool)
      gvn.svncmd.RunSvn(['add', '-q', change.relative_path])
  else:
    gvn.svncmd.RunSvn(['cp', '-qr', str(change.copyfrom_revision),
                   _wc.project.repository.URL(change.copyfrom_path),
                   change.relative_path])

def _MergeRemove(change, _wc, pool):
  gvn.svncmd.RunSvn(['rm', '-q', change.relative_path])

def _MergeReplace(change, _wc, pool):
  _MergeRemove(change, _wc, pool)
  _MergeAdd(change, _wc, pool)

def _MergeModify(change, _wc, pool):
    gvn.svncmd.RunSvn(['merge', '-q',
                   '@'.join([_wc.project.repository.URL(change.old_path),
                             str(change.old_revision)]),
                   '@'.join([_wc.project.repository.URL(change.path),
                             str(change.revision)]),
                   change.relative_path])

# TODO(epg): This isn't correct until (unless) we have a way to
# understand what R actions in changebranches mean (i.e. user
# modifies, not replaces, a file, but it's an R on the changebranch
# because of out-of-date top).  I haven't thought about that, but i
# think we can do it.  If not, this is going to require more work.
# Workaround for now is to changbranch from up-to-date working copy.

_MergeActions = {
  'A': _MergeAdd,
  'D': _MergeRemove,
  'R': _MergeReplace,
  'M': _MergeModify,
}

def Merge(cb, project, _wc, subpath, config, pool):
  """Merge ChangeBranch cb into WorkingCopy _wc; return a new ChangeBranch.

  The Config and Pool objects config and pool are used to create the new
  ChangeBranch object, which is then snapshotted and returned.  If
  the old ChangeBranch belongs to the user on whose behalf the caller
  is acting, return a new snapshotted ChangeBranch, but re-using the
  same underlying changebranch.

  """

  if len(cb.changed_paths) == 0:
    return cb

  _wc.Close()

  for change in cb.changed_paths:
    try:
      change_name = _wc.change_state[change.relative_path].change_name
    except KeyError:
      continue
    raise gvn.errors.ChangeBranch('%s already in %s'
                                   % (change.relative_path, change_name))

  changed_paths = []
  iterpool = svn.core.Pool(pool)
  for change in cb.changed_paths:
    iterpool.clear()
    _MergeActions[change.action](change, _wc, iterpool)
    if subpath == '':
      changed_paths.append(change.relative_path)
    else:
      changed_paths.append('/'.join([subpath, change.relative_path]))
  iterpool.destroy()

  if cb.username == _wc.project_config.username:
    # This is the calling user's own changebranch; reuse it.
    change_name = cb.name
  else:
    # This is someone else's changebranch; create one with the same
    # name (or uniquified, but close).
    change_name = UniqueChangeName(project, _wc.project_config.username, cb.name,
                                   pool)
  description = cb.description

  # TODO(epg): Pass our list of paths so it can open deeply.
  _wc.Open()
  cb = ChangeBranch(config, _wc.project, change_name)
  cb.Branch(_wc, changed_paths, description, pool)

  return cb
